package com.sunmouse.toollib.utils.load;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

import com.sunmouse.toollib.constants.Interaction;
import com.sunmouse.toollib.constants.Load;
import com.sunmouse.toollib.utils.LogUtils;
import com.sunmouse.toollib.utils.system.PermissionUtil;
import com.sunmouse.toollib.utils.ThreadPoolUtils;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;

/**
 * Created by Kvin on 2017/2/20.
 */
public class LoadUtils {
    public static final int READ_SIZE = 1024 * 1024;// 1M
    public static final int DOWNLOAD_LIMIT = 10 * READ_SIZE;//10M
    public static final int LOAD_ALL = -11;//load all
    private Handler mHandler;
    private int threadCount;
    private int taskIndex;
    private int finishCount;
    private int mMode;
    private int limitExecuteNum;
    private int currentExecuteNum;
    private int completeNum;
    private ArrayList<LoadRequest> mLoadPool;
    private LoadRequest mRequest;
    private long msgDelay;

    private OnLoadingListener mOnLoadingListener;

    public void setOnLoadingListener(OnLoadingListener listener) {
        this.mOnLoadingListener = listener;
    }


    public int getLength() {
        return (taskIndex < mLoadPool.size()) ? (int) mLoadPool.get(taskIndex).getLength() : 1;
    }

    public int getProgress() {
        return (taskIndex < mLoadPool.size()) ? (int) mLoadPool.get(taskIndex).getProgress() : 0;
    }

    /**
     * get load num
     */
    public int getLoadNum() {
        return mLoadPool.size();
    }

    /**
     * get waiting num
     */
    public int getWaitingNum() {
        return mLoadPool.size() - completeNum - currentExecuteNum;
    }

    /**
     * get free executor
     */
    public int getFreeExecutorNum() {
        return limitExecuteNum - currentExecuteNum;
    }


    /**
     * check whether there are enough free executors and needed waiting requests
     *
     * @return
     */
    private boolean isSupportedSchema() {
        return (currentExecuteNum < limitExecuteNum) && (getFreeExecutorNum() > 0) && (getWaitingNum() > 0);
    }

    /**
     * init in construct method
     */
    private LoadUtils(Context act, Builder builder, OnLoadingListener listener) {
        if (!PermissionUtil.isPermitted(act, Interaction.Permissions.FILE_ACCESS)) {
            throw new RuntimeException("Error: storage cannot be accessed");
        }
        initHandler();
        this.mLoadPool = new ArrayList<>();
        this.taskIndex = -1;
        this.limitExecuteNum = (builder.limitExecuteNum < 1 || builder.limitExecuteNum > 3) ? 3 : limitExecuteNum;//限制并发数
        this.mMode = builder.mode != Mode.MULTI_TASK ? Mode.SINGLE_TASK : builder.mode;
        this.msgDelay = builder.msgDelay < 1000 ? 1000 : builder.msgDelay;
        this.mOnLoadingListener = listener;
    }

    public static Builder init() {
        return new Builder();
    }

    /**
     * Builder
     */
    public static class Builder {
        private int mode;
        private int limitExecuteNum;
        private long msgDelay;
        private OnLoadingListener listener;

        /**
         * single task mode or multi task mode
         *
         * @param mode
         * @return
         */
        public Builder mode(int mode) {
            this.mode = mode;
            return this;
        }

        /**
         * @param limitExecuteNum
         * @return
         */
        public Builder limitExecuteNum(int limitExecuteNum) {
            this.limitExecuteNum = limitExecuteNum;
            return this;
        }

        /**
         * receive msg delay
         *
         * @param msgDelay
         * @return
         */
        public Builder msgDelay(long msgDelay) {
            this.msgDelay = msgDelay;
            return this;
        }

        public LoadUtils build(Context context) {
            return new LoadUtils(context, this, null);
        }

        public LoadUtils build(Context context, OnLoadingListener listener) {
            return new LoadUtils(context, this, listener);
        }

    }

    /**
     * stop
     */
    public synchronized void stop() {
        if (0 <= taskIndex && taskIndex < mLoadPool.size())
            mLoadPool.get(taskIndex).setStop(true);
    }

    /**
     * stop
     */
    public synchronized void stop(int index) {
        mLoadPool.get(index).setStop(true);
    }

    /**
     * stop all
     */
    public synchronized void stopAll() {
        for (int i = 0; i < mLoadPool.size(); i++) {
            mLoadPool.get(i).setStop(true);
        }
    }

    /**
     * start
     */
    public synchronized void start(int index) {
        this.mLoadPool.get(index).setStop(false);
        this.mLoadPool.get(index).setPosition(index);
        if (currentExecuteNum < limitExecuteNum) schemaRequest(mLoadPool.get(index));
    }

    /**
     * start all
     * load from index 0
     */
    public synchronized void startAll() {
        for (int i = 0; i < mLoadPool.size(); i++) {
            mLoadPool.get(i).setStop(false);
        }
        taskIndex = -1;
        nextRequest();
    }

    /**
     * cancel
     */
    public synchronized void cancel(int index) {
        if (taskIndex >= index) taskIndex--;//set point position
        mLoadPool.get(index).setStop(true);
        mLoadPool.remove(index);
    }


    /**
     * is loading
     */
    public synchronized boolean isStop(int index) {
        return mLoadPool.get(index).isStop();
    }

    /**
     * notify
     */
    public void notifyDataChange() {
        if (taskIndex >= 0 && taskIndex < mLoadPool.size()) {
            if (mLoadPool.get(taskIndex).isFinished() && !mLoadPool.get(taskIndex).isLoading()) {
                mLoadPool.get(taskIndex).setPosition(taskIndex);
                schemaRequest(mLoadPool.get(taskIndex));
            }
        }
    }

    /**
     * add request
     */
    public void addRequest(LoadRequest request) {
        if (request != null) mLoadPool.add(request);
    }

    /**
     * start loading
     */
    public void enqueue(OnLoadingListener listener) {
        if (listener != null)
            this.mOnLoadingListener = listener;
        this.taskIndex++;
        if (taskIndex < 0 || taskIndex >= mLoadPool.size()) return;
        LoadRequest request = mLoadPool.get(taskIndex);
        mLoadPool.get(taskIndex).setPosition(taskIndex);
        schemaRequest(request);
        nextRequest();
    }

    public void enqueue() {
        enqueue(null);
    }

    /**
     * schema request
     */
    public void schemaRequest(LoadRequest request) {
        this.finishCount = 0;
        this.currentExecuteNum++;
        ThreadPoolUtils.execute(mMode == Mode.SINGLE_TASK ? new SingleTaskRunnable(request) : new MultiTaskRunnable(request));
    }

    /**
     * init handler
     */
    private void initHandler() {
        if (mHandler == null)
            this.mHandler = new LoadHandler(Looper.getMainLooper());
    }

    /**
     * load runnable
     */
    private class SingleTaskRunnable implements Runnable {
        private LoadRequest request;

        public SingleTaskRunnable(LoadRequest request) {
            this.request = request;
        }

        @Override
        public void run() {
            try {
                URL url = new URL(request.getRemoteUrl());
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                if (conn.getResponseCode() == 200) {
                    int length = conn.getContentLength();
                    LogUtils.v("--length=" + length);
                    if (length == -1)
                        return;
                    double cpd = (double) length / DOWNLOAD_LIMIT;
                    int cpi = length / DOWNLOAD_LIMIT;
                    int count = cpd > cpi ? cpi + 1 : cpi;
                    sendMsg(Load.START, length > 0 ? length : 1, count);

                    LogUtils.v("---count=" + count);
                    for (int i = 0; i < count; i++) {//calculate num of threads
                        LoadTask.newTask(i * DOWNLOAD_LIMIT, DOWNLOAD_LIMIT, url, request.getOutFile(), mHandler).start();
                    }
                    conn.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * multi task runnable
     */
    private class MultiTaskRunnable implements Runnable {
        private LoadRequest request;

        public MultiTaskRunnable(LoadRequest request) {
            this.request = request;
        }

        @Override
        public void run() {
            try {
                URL url = new URL(request.getRemoteUrl());
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                if (conn.getResponseCode() == 200) {
                    int length = conn.getContentLength();
//                    LogUtils.v("--length=" + length);
                    if (length == -1)
                        return;
                    mLoadPool.get(request.getPosition()).setEnd(length);
                    mLoadPool.get(request.getPosition()).setLoading(true);
                    sendMsg(Load.START, length > 0 ? length : 1, request.getPosition());
                    MultiLoadTask.newTask(request, mHandler, request.getPosition()).start();
                    conn.disconnect();
                }
            } catch (IOException e) {
                sendMsg(Load.ERROR, e);
                e.printStackTrace();
            }
        }
    }

    /**
     * load handle
     */
    private class LoadHandler extends Handler {


        public LoadHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            int what=msg.what;
            if (what==Load.COMPLETE){
                completeNum++;
                mLoadPool.get(msg.arg2).setLoading(false);
                mLoadPool.get(msg.arg2).setFinished(true);
                currentExecuteNum--;
                nextRequest();
                if (mOnLoadingListener != null)
                    mOnLoadingListener.onComplete(msg.arg2);
                return;
            }
            if (what==Load.LOADING){
                mRequest = mLoadPool.get(msg.arg2);
                mRequest.setProgress(mRequest.getProgress() + msg.arg1);
                if (mOnLoadingListener != null)
                    mOnLoadingListener.onLoading((int) mRequest.getLength(), (int) mRequest.getProgress(), msg.arg2);
                if ((mRequest.getLength() == mRequest.getProgress()) && (finishCount == threadCount) && (mMode == Mode.SINGLE_TASK))
                    sendMsg(Load.COMPLETE, -1, -1);
//                    LogUtils.v("---length=" + mProgress + "/" + length + "--thread=" + finishCount + "/" + threadCount);
                return;
            }

            if (msg.what==Load.START){
                if (mMode == Mode.SINGLE_TASK)
                    threadCount = msg.arg2;
                if (mMode == Mode.MULTI_TASK) {
                    mLoadPool.get(msg.arg2).setLength(msg.arg1);
                    if (mOnLoadingListener != null) mOnLoadingListener.onStart(msg.arg2);
                }
                return;
            }

            if (msg.what==Load.STOP){
                currentExecuteNum--;
                mLoadPool.get(msg.arg2).setLoading(false);
                if (mOnLoadingListener != null)
                    mOnLoadingListener.onStop((int) mLoadPool.get(msg.arg2).getProgress(), msg.arg2);
            }
            if (what==Load.THREAD_FINISH){
                finishCount++;
                if (finishCount == threadCount) sendMsg(Load.LOADING, -1, -1);
                return;
            }
            if (what==Load.ERROR){
                if (mOnLoadingListener != null)
                    mOnLoadingListener.onError((Exception) msg.obj, msg.arg2);
            }
        }
    }

    /**
     * dispatch next request
     */
    private synchronized void nextRequest() {
        while (isSupportedSchema()) {
            if (completeNum == mLoadPool.size()) return;
            if (taskIndex < mLoadPool.size()) {
                taskIndex++;
                taskIndex = (taskIndex == mLoadPool.size()) ? 0 : taskIndex;
                if (!mLoadPool.get(taskIndex).isFinished()) {
                    mLoadPool.get(taskIndex).setPosition(taskIndex);
                    schemaRequest(mLoadPool.get(taskIndex));
                } else {
                    nextRequest();
                }
            }
        }
    }

    /**
     * release
     */
    public void release() {
        this.mHandler = null;
    }

    /**
     * send msg
     * read
     */
    private void sendMsg(int status, int read, int position, Object obj) {
        Message msg = Message.obtain();
        msg.what = status;
        msg.arg1 = read;
        msg.arg2 = position;
        msg.obj = obj;
        mHandler.sendMessageDelayed(msg, 1000);
    }

    private void sendMsg(int status, int read, int position) {
        this.sendMsg(status, read, position, null);
    }

    private void sendMsg(int status, Object obj) {
        this.sendMsg(status, -1, -1, obj);
    }


    /**
     * mode
     */
    public static final class Mode {
        public static final int SINGLE_TASK = 1;
        public static final int MULTI_TASK = 2;
    }


    public interface OnLoadingListener {
        /**
         * on start
         */
        void onStart(int position);

        /**
         * current info
         */
        void onLoading(int length, int progress, int position);

        /**
         * onStop
         */
        void onStop(int progress, int position);

        /**
         * finish listener
         */
        void onComplete(int position);

        /**
         * on error
         */
        void onError(Exception e, int position);

    }
}
